package org.bouscarlo.jj2000.decoding.service;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;

import jj2000.j2k.codestream.HeaderInfo;
import jj2000.j2k.codestream.reader.BitstreamReaderAgent;
import jj2000.j2k.codestream.reader.HeaderDecoder;
import jj2000.j2k.decoder.Decoder;
import jj2000.j2k.decoder.DecoderSpecs;
import jj2000.j2k.entropy.decoder.EntropyDecoder;
import jj2000.j2k.fileformat.reader.FileFormatReader;
import jj2000.j2k.image.BlkImgDataSrc;
import jj2000.j2k.image.Coord;
import jj2000.j2k.image.DataBlkInt;
import jj2000.j2k.image.ImgDataConverter;
import jj2000.j2k.image.invcomptransf.InvCompTransf;
import jj2000.j2k.io.BEBufferedRandomAccessFile;
import jj2000.j2k.io.RandomAccessIO;
import jj2000.j2k.quantization.dequantizer.Dequantizer;
import jj2000.j2k.roi.ROIDeScaler;
import jj2000.j2k.util.ISRandomAccessIO;
import jj2000.j2k.util.ParameterList;
import jj2000.j2k.wavelet.synthesis.InverseWT;
import colorspace.ColorSpace;

public class JJ2000Decoder {
	
	private ColorSpace csMap = null;
	private HeaderDecoder hd = null;
	
	private ParameterList pl;

	private HeaderInfo hi;

	private ParameterList defpl;

	protected JJ2000Decoder() {
        defpl = new ParameterList();
		String[][] param = Decoder.getAllParameters();

		for (int i=param.length-1; i>=0; i--) {
		    if(param[i][3]!=null)
			defpl.put(param[i][0],param[i][3]);
	        }

        // Create parameter list using defaults
        pl = new ParameterList(defpl);
	}



	
	/******************************************************************************************/
	/***************************************** Decoding ***************************************/
	/******************************************************************************************/

	/**
	 * Runs the decoder. After completion the exit code is set, a non-zero value
	 * indicates that an error ocurred.
	 * 
	 * @see #getExitCode
	 * */
	public BlkImgDataSrc decodeJP2(String infile)  throws Exception {

			RandomAccessIO in=null;
			// **** Open input files ****
			// Creates a BEBufferedRandomAccessFile of a ISRandomAccessIO
			// instance for reading the file format and codestream data
			if (infile.indexOf("/") >= 1 && infile.charAt(infile.indexOf("/") - 1) == ':') { // an URL
				URL inurl;
				URLConnection conn;
				int datalen;
				InputStream is;

					inurl = new URL(infile);
					conn = inurl.openConnection();
					conn.connect();
					datalen = conn.getContentLength();
					is = conn.getInputStream();
					if (datalen != -1) { // known length => initialize to length
						in = new ISRandomAccessIO(is, datalen, 1, datalen);
					} else { // unknown length => use defaults
						in = new ISRandomAccessIO(is);
					}
					in.read();
					in.seek(0);
			} else { // a normal file
					in = new BEBufferedRandomAccessFile(infile, "r");
			}
			return decodeJP2(in);
	}
	
	public BlkImgDataSrc decodeJP2(InputStream in)  throws Exception {
		return decodeJP2(new ISRandomAccessIO(in));
	}
	
	private BlkImgDataSrc decodeJP2(RandomAccessIO in) throws Exception {
			// **** File Format ****
			// If the codestream is wrapped in the jp2 fileformat, Read the
			// file format wrapper
			FileFormatReader ff = new FileFormatReader(in);
			ff.readFileFormat();
			if (ff.JP2FFUsed) {
				in.seek(ff.getFirstCodeStreamPos());
			}

			// +----------------------------+
			// | Instantiate decoding chain |
			// +----------------------------+

			// **** Header decoder ****
			// Instantiate header decoder and read main header
			hi = new HeaderInfo();
			hd = new HeaderDecoder(in, pl, hi);

			int nCompCod = hd.getNumComps();
			int nTiles = hi.siz.getNumTiles();
			DecoderSpecs decSpec = hd.getDecoderSpecs();

			// Report information
			{
				String info = nCompCod + " component(s) in codestream, "+ nTiles + " tile(s)\n";
				info += "Image dimension: ";
				for (int c = 0; c < nCompCod; c++) {
					info += hi.siz.getCompImgWidth(c) + "x"
							+ hi.siz.getCompImgHeight(c) + " ";
				}

				if (nTiles != 1) {
					info += "\nNom. Tile dim. (in canvas): " + hi.siz.xtsiz
							+ "x" + hi.siz.ytsiz;
				}
				System.out.println(info);
			}
			
			
			// Get demixed bitdepths
			int[] depth = new int[nCompCod];
			for (int i = 0; i < nCompCod; i++) {
				depth[i] = hd.getOriginalBitDepth(i);
			}

			// **** Bit stream reader ****
			BitstreamReaderAgent breader = BitstreamReaderAgent.createInstance(in, hd, pl,
						decSpec, pl.getBooleanParameter("cdstr_info"), hi);
			
			// **** Entropy decoder ****
			EntropyDecoder entdec = hd.createEntropyDecoder(breader, pl);
			

			// **** ROI de-scaler ****
			ROIDeScaler roids = hd.createROIDeScaler(entdec, pl, decSpec);
			
			// **** Dequantizer ****
			Dequantizer deq = hd.createDequantizer(roids, depth, decSpec);
			
			// **** Inverse wavelet transform ***
			// full page inverse wavelet transform
			InverseWT invWT = InverseWT.createInstance(deq, decSpec);

			int res = breader.getImgRes();
			invWT.setImgResLevel(res);

			// **** Data converter **** (after inverse transform module)
			ImgDataConverter converter = new ImgDataConverter(invWT, 0);

			// **** Inverse component transformation ****
			InvCompTransf ictransf = new InvCompTransf(converter, decSpec, depth, pl);

			// **** Color space mapping ****
			if (ff.JP2FFUsed && pl.getParameter("nocolorspace").equals("off")) {
				csMap = new ColorSpace(in, hd, pl);
				BlkImgDataSrc channels = hd.createChannelDefinitionMapper(ictransf, csMap);
				BlkImgDataSrc resampled = hd.createResampler(channels, csMap);
				BlkImgDataSrc palettized = hd.createPalettizedColorSpaceMapper(resampled,csMap);
				BlkImgDataSrc color = hd.createColorSpaceMapper(palettized, csMap);
				return color;
			} else { // Skip colorspace mapping
				return ictransf;
			}
	}
		
	

	
	/******************************************************************************************/
	/***************************************** Encoding ***************************************/
	/******************************************************************************************/
	
	
	
	
	private boolean PPM=false;

    /** Value used to inverse level shift. One for each component */
    private int[] levShift = new int[3];

    /** The array of indexes of the components from where to get the data */
    private int cps[] = new int[3];

    /** The array of the number of fractional bits in the components of the
        source data */
    private int fb[] = new int[3];

    /** A DataBlk, just used to avoid allocating a new one each time
        it is needed */
    private DataBlkInt db = new DataBlkInt();

    /** The offset of the raw pixel data in the PPM file */
    private int offset;
    
    /** The line buffer. */
    // This makes the class not thrad safe
    // (but it is not the only one making it so)
    private byte buf[];
    
    private int data[];
    private int nomRangeBits;
    
    private int w,h;
    
    private BlkImgDataSrc src;
    
    /** The defaukt height used when writing strip by strip in the 'write()'
     * method. It is 64. */
    public final static int DEF_STRIP_HEIGHT = 64;

    protected JJ2000Bitmap encodeBMP(BlkImgDataSrc imgSrc) throws IOException{
    	
    	PPM=(imgSrc.getNumComps()==3);
    	if (PPM)
    		initPPM(imgSrc);
    	else 
    		initPGX(imgSrc);
    	
    	encodeBMP();
    	
    	return new JJ2000Bitmap(w, h, data);
    }
	
    private void initPPM(BlkImgDataSrc imgSrc) throws IOException{
    	
        // Check that imgSrc is of the correct type
        // Check that the component index is valid
        if((imgSrc.getNomRangeBits(0)>8) || 
		   (imgSrc.getNomRangeBits(1)>8) ||
		   (imgSrc.getNomRangeBits(2)>8)) {
	            throw new IllegalArgumentException("Invalid component indexes");
        }
        
        nomRangeBits=imgSrc.getNomRangeBits(0);
        
        // Initialize
        w = imgSrc.getCompImgWidth(0);
        h = imgSrc.getCompImgHeight(0);
        // Check that all components have same width and height
        if(w!=imgSrc.getCompImgWidth(1) ||
		   w!=imgSrc.getCompImgWidth(2) ||
		   h!=imgSrc.getCompImgHeight(1) ||
		   h!=imgSrc.getCompImgHeight(2)) {
            throw new IllegalArgumentException("All components must have the"+
					       " same dimensions and no"+
					       " subsampling");
        }
		w = imgSrc.getImgWidth();
		h = imgSrc.getImgHeight();

	
        src = imgSrc;
        cps[0] = 0;
        cps[1] = 1;
        cps[2] = 2;
        fb[0] = imgSrc.getFixedPoint(0);
        fb[1] = imgSrc.getFixedPoint(1);
        fb[2] = imgSrc.getFixedPoint(2);
        
        levShift[0] = 1<< (imgSrc.getNomRangeBits(0)-1);
        levShift[1] = 1<< (imgSrc.getNomRangeBits(1)-1);
        levShift[2] = 1<< (imgSrc.getNomRangeBits(2)-1);
        
        data=new int[w*h];

    }
    
    private void initPGX(BlkImgDataSrc imgSrc) throws IOException{
    	
    	src = imgSrc;
        w = src.getImgWidth();
        h = src.getImgHeight();
        
        nomRangeBits = src.getNomRangeBits(0);
        if((nomRangeBits<=0)||(nomRangeBits>31)) {
	            throw new IOException("PGX supports only bit-depth between "+
	                                  "1 and 31");
		}
        
        cps[0] = 0;
        fb[0] = imgSrc.getFixedPoint(0);
        
        levShift[0] = 1<< (imgSrc.getNomRangeBits(0)-1);
        
        data=new int[w*h];
    }


    
    /**
     * Writes the source's current tile to the output. The requests of data
     * issued to the source BlkImgDataSrc object are done by strips, in order
     * to reduce memory usage.
     *
     * <P>If the data returned from the BlkImgDataSrc source is progressive,
     * then it is requested over and over until it is not progressive any
     * more.
     *
     * @exception IOException If an I/O error occurs.
     * */
    public void encodeBMP() throws IOException {
    	
    	// Find the list of tiles to decode.
    	Coord nT = src.getNumTiles(null);

    	// Loop on vertical tiles
    	for (int y = 0; y < nT.y; y++) {
    		// Loop on horizontal tiles
    		for (int x = 0; x < nT.x; x++) {
    			try {
    				src.setTile(x, y);
    			} catch (Exception eofe) {
    			}
    		}
    	}
    	
        int i;
        int tIdx = src.getTileIdx();
        int tw = src.getTileCompWidth(tIdx,0);  // Tile width 
        int th = src.getTileCompHeight(tIdx,0);  // Tile height
        // Write in strips
        for(i=0; i<th ; i+=DEF_STRIP_HEIGHT) {
        	if (PPM)
        		encodePPM(0,i,tw,((th-i)<DEF_STRIP_HEIGHT) ? th-i : DEF_STRIP_HEIGHT);
        	else 
        		encodePGX(0,i,tw,((th-i)<DEF_STRIP_HEIGHT) ? th-i : DEF_STRIP_HEIGHT);
        }
    }
    
    
    /**
     * Writes the data of the specified area to the file, coordinates are
     * relative to the current tile of the source. Before writing, the
     * coefficients are limited to the nominal range.
     *
     * <p>This method may not be called concurrently from different
     * threads.</p>
     *
     * <p>If the data returned from the BlkImgDataSrc source is progressive,
     * then it is requested over and over until it is not progressive
     * anymore.</p>
     *
     * @param ulx The horizontal coordinate of the upper-left corner of the
     * area to write, relative to the current tile.
     *
     * @param uly The vertical coordinate of the upper-left corner of the area
     * to write, relative to the current tile.
     *
     * @param width The width of the area to write.
     *
     * @param height The height of the area to write.
     *
     * @exception IOException If an I/O error occurs.
     * */
    private void encodePPM(int ulx, int uly, int w, int h) throws IOException{
    	
        int k,j,i,c;
        // In local variables for faster access
        int fracbits;
        // variables used during coeff saturation
        int shift,tmp,maxVal;
        int tOffx, tOffy;      // Active tile offset in the X and Y direction
        
        // Active tiles in all components have same offset since they are at
        // same resolution (PPM does not support anything else)
        tOffx = src.getCompULX(cps[0]) -
            (int)Math.ceil(src.getImgULX()/(double)src.getCompSubsX(cps[0]));
        tOffy = src.getCompULY(cps[0]) -
            (int)Math.ceil(src.getImgULY()/(double)src.getCompSubsY(cps[0]));

//        System.out.println("PPM "+ulx+" "+uly+" "+w+" "+h+" "+tOffx+" "+tOffy);

        // Check the array size
        if(db.data!=null && db.data.length<w) {
            // A new one will be allocated by getInternCompData()
            db.data = null;
        }
        
        // Check the line buffer
        if(buf==null || buf.length<3*w) {
            buf = new byte[3*w];
        }

        // Write the data to the file
        // Write line by line
        for(i=0; i<h; i++) {
            // Write into buffer first loop over the three components and
            // write for each
            for(c=0; c<3; c++) {
                maxVal= (1<<src.getNomRangeBits(cps[c]))-1;
                shift = levShift[c];
            
                // Initialize db
                db.ulx = ulx;
                db.uly = uly+i;
                db.w = w;
                db.h = 1;

                // Request the data and make sure it is not progressive
                do {
                    db = (DataBlkInt)src.getInternCompData(db,cps[c]);
                } while (db.progressive);
                // Get the fracbits value
                fracbits = fb[c];
                // Write all bytes in the line
                if(fracbits==0) {
                    for(k=db.offset+w-1, j=3*w-1+c-2; j>=0; k--) {
                        tmp = db.data[k]+shift;
                        buf[j] = (byte)((tmp<0)? 0 : ((tmp>maxVal)?
                                                      maxVal : tmp));
                        j -= 3;
                    }
                } else {
                    for(k=db.offset+w-1, j=3*w-1+c-2; j>=0; k--) {
                        tmp = (db.data[k]>>>fracbits)+shift;
                        buf[j] = (byte)((tmp<0)? 0 : ((tmp>maxVal)?
                                                      maxVal : tmp));
                        j -= 3;
                    }
                }   
            }
            writePPM(offset+3*(w*(uly+tOffy+i)+ulx+tOffx),buf);//,0,3*w
        }
    }
    
    private void writePPM(int pos, byte[] buf){
//        System.out.println("Writing "+buf.length+" at "+pos);
        pos=pos/3;
        for (int i=0;i<buf.length;i+=3, pos+=1){
        	byte r=buf[i];
        	byte g=buf[i+1];
        	byte b=buf[i+2];
        	data[pos]= 0xFF000000 | (r & 0xFF) << (2 * nomRangeBits) | (g & 0xFF) << nomRangeBits | (b & 0xFF);
        }
    }

    
    private void encodePGX(int ulx, int uly, int w, int h) throws IOException{
       	    	
        int k,j,i,c;
        // In local variables for faster access
        int fracbits;
        // variables used during coeff saturation
        int shift,tmp,maxVal;
        int tOffx, tOffy;      // Active tile offset in the X and Y direction
        
        // Active tiles in all components have same offset since they are at
        // same resolution (PPM does not support anything else)
        tOffx = src.getCompULX(cps[0]) -
            (int)Math.ceil(src.getImgULX()/(double)src.getCompSubsX(cps[0]));
        tOffy = src.getCompULY(cps[0]) -
            (int)Math.ceil(src.getImgULY()/(double)src.getCompSubsY(cps[0]));

//        System.out.println("PGX "+ulx+" "+uly+" "+w+" "+h+" "+tOffx+" "+tOffy);
        
        // Check the array size
        if(db.data!=null && db.data.length<w) {
            // A new one will be allocated by getInternCompData()
            db.data = null;
        }
        
        // Check the line buffer
        if(buf==null || buf.length<w) {
            buf = new byte[w];
        }

        // Write the data to the file
        // Write line by line
        for(i=0; i<h; i++) {
            // Write into buffer first loop over the three components and
            // write for each
            for(c=0; c<1; c++) {
                maxVal= (1<<src.getNomRangeBits(cps[c]))-1;
                shift = levShift[c];
//                System.out.println(" Max "+maxVal+" Shift "+shift+" Fracbits "+fb[c]);
                // Initialize db
                db.ulx = ulx;
                db.uly = uly+i;
                db.w = w;
                db.h = 1;

                // Request the data and make sure it is not progressive
                do {
                    db = (DataBlkInt)src.getInternCompData(db,cps[c]);
                } while (db.progressive);
                // Get the fracbits value
                fracbits = fb[c];
                // Write all bytes in the line
                if(fracbits==0) {
                    for(k=db.offset+w-1, j=w-1+c; j>=0; k--) {
                        tmp = db.data[k]+shift;
                        buf[j] = (byte)((tmp<0)? 0 : ((tmp>maxVal)?
                                                      maxVal : tmp));
                        j -= 1;
                    }
                } else {
                    for(k=db.offset+w-1, j=w-1+c; j>=0; k--) {
                        tmp = (db.data[k]>>>fracbits)+shift;
                        buf[j] = (byte)((tmp<0)? 0 : ((tmp>maxVal)?
                                                      maxVal : tmp));
                        j -= 1;
                    }
                }   
            }
            writePGX(offset+(w*(uly+tOffy+i)+ulx+tOffx),buf);
        }
    }
    
    private void writePGX(int pos, byte[] buf){
//        System.out.println("Writing "+buf.length+" at "+pos);
        for (int i=0;i<buf.length;i+=1, pos+=1){
        	byte r=buf[i];
        	byte g=buf[i];
        	byte b=buf[i];
        	data[pos]= 0xFF000000 | (r & 0xFF) << (2 * nomRangeBits) | (g & 0xFF) << nomRangeBits | (b & 0xFF);
        }
    }
}
